今天開始正式進入系列文的最後一個章節 - Framework, Architecture and Memory Management,探索前端架構與底層實作對於效能的影響。
提到前端的架構,又或者說技術,CSR
、SSR
、SSG
是三個最常出現的名詞,你可能曾經聽過這樣一句話:「使用 Server Side Rendering 相較於 CSR 可以加快網頁的效能。」然而事情真的是這樣嗎?今天將來好好認識一下這些最常見的 Web Rendering 的架構,再分析他們與前端效能有什麼關係。
在前端框架(React、Vue、Angular)盛行後,SPA (Single-Page-Application) 就如雨後春筍般冒出,而它的運作概念是這樣的,以往會被塞滿各種 element 的 HTML 檔,如今只會放入一個 tag 當作「容器」而已,例如以下範例:(以 React 為例子)
<html>
<head></head>
<body>
<div id="root"></div>
<script src="./script.js"></script>
</body>
</html>
上面 id 為 root 的 div 即成為了上面提到的容器,script.js 則是 react code ,採用這種方式後,頁面的元素都將交由 react 去渲染出來並塞進容器中。
server 回傳只包含容器的 HTML -> 瀏覽器依據 HTML 下載 JS code -> 瀏覽器執行 React code -> 執行完畢後,頁面才完整呈現與具有互動性
採用這種方式後,我們可以不必像以前ㄧ樣準備ㄧ大堆的 HTML 檔案,而是透過 Router 決定該渲染出哪些元素在畫面上或是該抓哪些 API data,而 routing 的過程也不再像以往ㄧ樣要重新去 server 抓取頁面造成頁面反白,使用者體驗大大的提升,這就是 Client Side Rendering。
看似美好,然而,它也產生了一些問題。
SEO 對很多企業網站來說是很重要的指標,而它很大一部分得靠搜尋引擎的爬蟲爬取網站的資訊,問題出在上面說的,我們的 HTML 現在只有容器在裡面而已,其他內容都是由 JS 動態產生的,這會造成爬蟲在爬取資料時只會爬到空蕩蕩的幾個 tag…,也因此造成 SEO 分數低落。(有興趣的讀者可以在想觀察的網頁點選右鍵:檢查網頁原始碼)。
其實 CSR 架構還是有辦法做 SEO 優化的,例如可以在 Web Server 例如 Nginx 檢測 request agent 是不是爬蟲引擎,是的話就回傳一個另外準備好的填滿內容的 HTML file 給爬蟲程式,如果 agent 不是搜尋引擎爬蟲就回傳原本要給 CSR 用的容器檔案再到前端做渲染。還有一種方式是在 lambda@Edge 或是 Cloudflare Wrokers 這類的 edge serverless 服務做 pre-rendering,有興趣的讀者可以看看這篇文章。
(另外搜尋引擎也變得越來越強了,例如 google search engine 就發展出了一個名為 second wave of indexing 的技術,有興趣的讀者可以看這篇文章,未來 SEO 也許就不再會是 CSR 的一個痛點,不過要等到所有搜尋引擎都能做到這點可能需要一段時間。)
上面提到 CSR 的步驟是瀏覽器載入 JS 後再去執行它,最後靠 JS 才能渲染出要顯示的元件與交互,但是如果你的 JS 隨著專案的擴充變得非常肥大呢?瀏覽器在下載 JS 與執行 JS 上所花的時間都會因此增加,儘管現在瀏覽器已經非常快速了,仍然會對效能造成影響,而因為 CSR 是得在 JS 執行完畢時才能顯示出整個網頁,上述流程的 delay 連帶的影響到使用者等待頁面顯示的時間,(當然我們可以做 code-spliting、dynamic-import 等解決 bundle size 過於肥大的優化,但這些技巧能解決的問題是有限的。)
其實在過去,網頁幾乎都是透過 Server Side Rendering 的方式產生的,如果你用傳統 PHP 寫過網頁,就知道會是透過在伺服器端處理好資料與邏輯,再直接編譯成 HTML 檔案,回傳時使用者看到的就會是完整包含資料的 HTML。
這種架構的問題很明顯,就是換頁時都會經過反白與閃爍,萬一網頁效能又不太好,使用者很容易因為糟糕的使用者體驗選擇直接離開頁面。
承如大家所知,CSR 的出現解決了換頁時使用者體驗不佳的問題,不過卻衍生出了前面提過的 SEO 問題。
後來不同於傳統 SSR 的架構出現了,它通常被稱作 Isomorphic SSR,或是混合式 SSR。
Isomorphic SSR 仍然保持了 SPA 換頁時不會閃爍的優點,並同時考慮 SSR 與 CSR 兩種渲染方式,在 server side 先渲染出帶有基本資料的 HTML 檔案,送到前端後再進行 hydration,讓網站產生可互動性。因為在 server side 就先產生了基本的資料,因此 SEO 的問題就可以得到解決。
至於 hydration 的意思則是會在 client 把 server side 先渲染出的 DOM element 加上事件監聽器等屬性,讓 DOM element 變為動態且具有互動性,能夠響應後續的資料變化。
Isomorphic SSR(本文後面都將直接稱之為 SSR)與 CSR 架構最大的差別是 SSR 不必等到 JS 執行完畢後才能讓使用者看到畫面,在 server 回傳 HTML 後使用者就能夠看到頁面,即使因為 JS 還沒被執行,所以畫面還不具備互動性,但讓使用者先看到畫面,再利用人的感知延遲時間去執行 JS code,可以大大減少使用者的跳出率。
有利於 SEO 分數、可以動態取得資料、使用者可以提早看到畫面,看起來...SSR 超棒的啊...!!
不要忘記 SSR 需要維護一個 server 來接收請求在伺服器端產生內容,這個 server 通常被稱為 rendering server。Isomorphic SSR 的精髓在於希望 client side 與 server side 可以共用一部分程式碼,所以這個 rendering server 通常會是 Node.js 的伺服器。而維護一個伺服器是需要成本的,尤其在遇到大流量時,對伺服器來說就是一個負擔。
另外使用 SSR 架構與邏輯會變得十分複雜,許多程式會需要分別在 server 與 client 做處理,也要特別小心不能在 server 端執行到包含如 window 等 browser 的 API,我認為會將專案複雜度往上提高一個層級。
聰明的你應該還會想到一個問題,如果網頁的內容不常變動,用 SSR 架構的話每次都要重新在 server side 產生頁面,即便可以做快取來避免重複渲染一樣的內容,但感覺還有更好的處理方式才對。這種狀況下,SSG 就是一個蠻適合的技術。
在 CSR, SSR, SSG 三者之中,SSG 是效能最好的。
Static Site Generation 意指在 build time 時就打包成一個擁有完整內容的 HTML 檔案,並且在之後的 client request 都會共用這個 HTML,這種方式也被稱作 pre-rendering。
採用這種方式的優點為效能方面,因為它有較好的快取機制,例如很適合放到 CDN 上,當然,因為事先產生好內容,所以也有利於 SEO。不過它的限制也十分明顯,因為頁面資料的抓取(如 API call)只能透過 application build 的時候,如果需要變換內容,就得重新 build 一次,因此比較適合使用在內容不需要經常變換的頁面,例如部落格、形象網站這種應用。而當 Web App 越來越肥大時,打包的時間會隨之增長也是需要考量的問題。
SSG 雖然效能很好,但也暴露了一個問題,因為必須在 build time 就打包好網站的頁面,所以如果當你的網站有幾千個幾萬個頁面...例如陳列商品的網站,一但數據有改動,需要重新打包網站,這個改動是很可怕的,因此後來就出現了一些新的架構想要來解決 SSG 的問題,今天要介紹的 ISR 就是其中一種。
ISR 最早是由 Next.js 9.5 版時提出的概念,是一種混用 SSR 與 SSG 的技術。如果頁面有幾百頁幾千頁,那其實並不一定要急著在 build time 就打包出所有的頁面,有些頁面可以在 runtime 時再產生。咦,那這樣跟 SSR 差在哪啊?如果是 SSR 的話,每一次發出對頁面的 request 時,使用者必須等待頁面資料抓取完畢才能看到畫面,而如果是 ISR 的話,第一次對頁面發出 request 時,會先回傳 fallback 的畫面給使用者,這個 fallback 頁面可能包含一些 placeholder 與 skeleton,在呈現 fallback 頁面時,同時也會去抓取需要的資料,等資料準備好後,完整的頁面可以被拿到 CDN 做快取,後續的使用者如果對同樣頁面發出請求,就可以直接到 CDN 拿取,就像使用 SSG 的時候一樣。
在 Next.js 裡,還可以指定什麼時候要 re-validate data 並更新頁面,在 re-validate 的期間,會先回傳先前被 cache 的版本,等資料更新後才會切換到更新後的頁面,並更新 CDN 中的快取。這樣的快取策略也被稱作「stale-while-revalidate」。
// 在 Next.js 中要啟用 ISR 很簡單,跟 SSG 的寫法很像,只是要多指定需要 revalidate 的時間,下面的範例代表每 60 秒就會 revalidate 一次頁面。
export async function getStaticProps() {
const res = await fetch('https://...');
const data = await res.json();
return { props: { data }, revalidate: 60 };
}
如果讀者擅長的框架是 Vue.js 的話,Nuxt.js 也有 ISR 相關的解決方案,有興趣可以參考這篇文章。
過去混合式 SSR 會遇到一些問題,造成網頁效能出現一些瓶頸:
Streaming server rendering 的出現就是為了解決這些問題,Streaming server rendering 讓伺服器可以用 streaming 的方式傳送頁面內容,讓瀏覽器可以漸進的去接收 chunks,不用等整份 HTML 渲染完成,會有比較快的 FP, FCP(稍後會提到這些指標)。Progressive Rehydration 則可以做到不用等到所有 component 都 hydrate 完就能夠讓部分元件與使用者互動,例如以下這張圖。
未來預計推出的 React v18 就推出了 Suspense SSR 的概念,藉由 Suspense Component 的幫助來達成以元件為單位的 streaming Sever Side Rendering 與 hydration,提升畫面呈現速度跟可互動速度,有興趣的讀者可以看看 React 官方的 Github 討論串,真的十分精彩。
還記得在 Day04 的時候介紹過一些跟網站效能相關的指標嗎?今天想藉這個機會再補充幾個與效能相關的指標:
TTFB 這個指標關係到 network speed、server response time,代表發出頁面請求後到接收到 response 的第一個 byte 的時間總長度。這個過程包含了 DNS resolve, TCP connection, 發出 HTTP request 到獲得 HTTP Response 第一個 byte 的時間。
任何一個 pixel(像素)被瀏覽器繪製到頁面上的時間,例如頁面的 backgorund color。
中文為「首次內容繪製」,當瀏覽者到達網站之後,首次顯示網站內容需要的時間,也是指瀏覽器第一次顯示文字或圖片的時間,測試第一次顯示原因是第二次再顯示網站的時候,瀏覽器已經有快取檔案了,因此會沒有那麼準確。
頁面從不能互動到可以接收事件產生互動性的時間,例如使用者除了可以看到頁面外還能進行輸入的操作。
了解這些指標後,接著來看看各種渲染架構對這些指標造成的影響。(內容主要出自 Google 非常有名的這篇文章)
指的是以前由伺服器端渲染出完整 HTML 的 SSR 架構。這樣的架構會有較快的 FP 與 FCP,另外因為避免在 client 發送大量的 JavaScript,因此也會有較快 TTI。
傳統的 SSR 的缺點也很明顯,因為要在 server side 生成完整頁面,因此 Time To First Byte (TTFB) 的時間會比較久。
CSR 直接使用 JavaScript 在瀏覽器上渲染页面,如果隨著專案擴展, JavaScript 的 bundle size 過於肥大,將會嚴重影響 FCP 與 TTI,使用者可能長時間看到的是空白或者不完整、還無法互動的頁面。
另外如果是在硬體效能比較差的 mobile device 跑 CSR 的網頁,有時候在 Client Side 利用 JavaScript 渲染內容會是一個蠻大的負擔。
要解決 CSR 的這些問題,可以使用先前介紹過的 Code Splitting 與 Lazy Loading 等拆分 bundle 或延後載入的技術,另外也可以透過 preload resource hint 或是 HTTP2 的 server push 來加快資源的載入時間。
相信大家應該已經很了解 SSG 與它的優缺點了,這邊就不再贅述。
混合式的 SSR 的 FCP 會比 CSR 來得快,但因為需要在前端做 hydration 之後畫面才會具有互動性,因此 TTI 可能會變很長。使用這種架構時要注意在 server side 不要處理太多的資料,建議是把 SSR 用在渲染一些可以被 cache 的資源,除了可以簡短 TTFB 之外,也有機會達到接近 pre-rendering 的速度。
最後附上一張 Google 發布的圖表來統整不同架構的優缺點與相關資訊。
在本篇開頭有提到說你可能聽過這樣一句話:「使用 Server Side Rendering 相較於 CSR 可以加快網頁的效能。」事情真的是如此嗎?
It depands. 需要視情況而論。
使用 SSR 的確有機會讓頁面載入速度快一點,比起 CSR 更有機會讓使用者先看到畫面而減少跳出率。不過如果在 server side 要處理的事情太多,反倒會讓網頁的載入速度變得緩慢,因此建議在使用 SSR 時只需要在 server side 處理對 SEO 相對重要的資料,其他的部分可以在 client side 用 CSR 的方式渲染,畢竟這也是混合式 SSR 的一大特色。
至於 SSR 與 SSG 之間的取捨,SSG 的效能的確比較好,因為它在 build 的時候就已經產生資料,使用者發起對頁面的請求時,伺服器可以直接回應已經產生好的 HTML 檔案,並且這個經過 pre-rendering 的網站還適合放到 CDN 做快取,加快後續請求的回應速度。但當頁面是有頻繁變動的需求時,就不太適合 SSG 的方式了,此時採用 SSR 呼叫 API 動態產生內容就是比較適合的方式,不過 SSR 架構下就要去維護一個 rendering server 並 handle server 面對大流量的狀況,因此我覺得仍然得看自身狀況去做架構的取捨。
幸好現在許多的 Server Side Rendering 框架,例如 Next.js,已經支援 page level 的架構選擇,也就是說不同頁面可以針對需求決定要使用 CSR, SSR, SSG, 還是 ISR 等不同的渲染架構,真的非常方便,如果想更深入了解 Next.js 這種 SSR 框架,可以訂閱團隊夥伴 Airwaves 的系列文喔!
今天大致介紹了幾種常見的 Web 前端應用架構(又或者說是技術),每一種架構都有各自的優缺點與適合使用的時機,學會在適合的時機使用適合的架構,對網站的效能會產生很大的影響,這也是我把這些渲染技術列入系列文主題的原因。
通常在談 Web 渲染架構時,大多只會得到 CSR, SSR, SSG 三種較常見與成熟的架構,近期 ISR 可能也漸漸嶄露頭角,不過我明天想要再介紹一個較冷門且尚未成熟的架構--ESR,看看它的概念是什麼與對網頁效能的影響,明天見囉!
https://medium.com/tiny-code-lessons/client-side-static-and-server-side-rendering-e2769c381c09
https://blog.logrocket.com/incremental-static-regeneration-with-next-js/
https://arunoda.me/blog/what-is-nextjs-issg
https://github.com/reactwg/react-18/discussions/37